// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ImageOperations2D.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Microsoft.Robotics.Vision
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Drawing;
    using System.IO;
    using System.IO.Compression;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;

    using Microsoft.Robotics.Numerics;
    using Microsoft.Robotics.Vision.Cameras.Webcam;
    
    /// <summary>
    /// A class to hold methods/functions related to low-level 2D image processing.  This includes, e.g.,
    /// image conversion, color space conversion, convolution, flipping/flopping, rescaling, warping, etc.
    /// </summary>
    public static class ImageOperations2D
    {
        /// <summary>
        /// Downscale image by a given factor (both columns and rows)
        /// </summary>
        /// <typeparam name="T">Pixel type</typeparam>
        /// <param name="originalFrame">Original frame</param>
        /// <param name="originalSize">Original image size</param>
        /// <param name="downScaleFactor">Downscaling factor</param>
        /// <returns>Downscaled image and new size</returns>
        public static Tuple<T[], Size> Downscale<T>(T[] originalFrame, Size originalSize, int downScaleFactor)
        {
            Size newSize = new Size(originalSize.Width / downScaleFactor, originalSize.Height / downScaleFactor);

            T[] transformedFrame = new T[newSize.Width * newSize.Height];            

            int transformedFrameIndex = 0;

            for (int i = 0; i < originalSize.Height; i += downScaleFactor)
            {
                int rowInOriginal = i * originalSize.Width;

                for (int j = 0; j < originalSize.Width; j += downScaleFactor)
                {
                    transformedFrame[transformedFrameIndex] = originalFrame[rowInOriginal + j];
                    ++transformedFrameIndex;
                }
            }

            return new Tuple<T[], Size>(transformedFrame, newSize);
        }

        /// <summary>
        /// Constructs a normalized 1D Gaussian convolution kernel
        /// </summary>
        /// <param name="sigma">standard deviation of the Gaussian</param>
        /// <returns>The Gaussian convolution kernel</returns>
        public static double[] Construct1DGaussianKernel(double sigma)
        {
            const double NumberOfStandardDeviations = 2.5;  // Note:  2.5 means capture 98.76% of the curve
            int halfWidth = (int)Math.Round(NumberOfStandardDeviations * sigma - 0.5);
            int width = 2 * halfWidth + 1;
            double[] kernel = new double[width];
            double sum = 0;

            for (int i = 0; i < width; i++)
            {
                kernel[i] = Math.Exp(-(i - halfWidth) * (i - halfWidth) / (2 * sigma * sigma));
                sum += kernel[i];
            }

            for (int i = 0; i < width; i++)
            {
                kernel[i] /= sum;
            }

            return kernel;
        }

        /// <summary>
        /// Constructs a normalized 2D isotropic Gaussian convolution kernel
        /// </summary>
        /// <param name="sigma">standard deviation of the Gaussian</param>
        /// <returns>The Gaussian convolution kernel</returns>
        public static double[,] Construct2DGaussianKernel(double sigma)
        {
            int halfWidth = (int)Math.Round(2.5 * sigma - 0.5);
            int halfHeight = halfWidth;
            int width = 2 * halfWidth + 1;
            int height = width;

            double[,] kernel = new double[width, height];
            double sum = 0;

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    double squaredDistance = (x - halfWidth) * (x - halfWidth) + (y - halfHeight) * (y - halfHeight);
                    double val = Math.Exp(-squaredDistance / (2 * sigma * sigma));
                    kernel[x, y] = val;
                    sum += val;
                }
            }

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    kernel[x, y] /= sum;
                }
            }

            return kernel;
        }

        /// <summary>
        /// Automatically compute the sigma for the range Gaussian for a particular depth
        /// </summary>
        /// <param name="depth">The depth of the pixel for which to compute the range sigma</param>
        /// <param name="nominalDepth">The depth at which the returned value is equal to the nominal range sigma</param>
        /// <param name="nominalRangeSigma">The nominal range sigma</param>
        /// <returns>The range sigma automatically adjusted for depth</returns>
        public static double AutoComputeRangeSigma(double depth, double nominalDepth, double nominalRangeSigma)
        {
            double relativeDepth = (depth - nominalDepth) / nominalDepth;
            return nominalRangeSigma * (1 + relativeDepth * relativeDepth);
        }

        /// <summary>
        /// Performs bilateral filtering on a depth image using the standard, obvious, slow implementation
        /// </summary>
        /// <param name="image">Input depth image (assumed to be in millimeters)</param>
        /// <param name="spatialSigma">standard deviation of spatial Gaussian (in pixels, recommended value:  0.7)</param>
        /// <param name="rangeSigma">standard deviation of range Gaussian (in millimeters, recommended value:  10.0) </param>
        /// <param name="autoAdjustRangeSigma">if true, then the standard deviation of the range Gaussian is computed automatically, using 'rangeSigma' as the value for a depth of 1 meter</param>
        /// <returns>The bilaterally filtered depth image</returns>
        public static ImageFrameDepth BilateralFilterExact(ImageFrameDepth image, double spatialSigma, double rangeSigma, bool autoAdjustRangeSigma)
        {
            ImageFrameDepth filtered = new ImageFrameDepth(image.Width, image.Height);
            double[,] spatialKernel = Construct2DGaussianKernel(spatialSigma);
            int border = (spatialKernel.GetLength(0) - 1) / 2;

            const double NominalDepthForRangeSigma = 1000;  // This constant is the depth (in millimeters) at which 'rangeSigma' is used without alteration
            double rangeSigmaForThisPixel = rangeSigma;

            Parallel.For(
                border,
                image.Height - border,
                y =>
            {
                for (int x = border; x < image.Width - border; x++)
                {
                        double depthAtPixel = image[x, y];
                        if (depthAtPixel >= 0)
                        {
                            // Compute sigma for range Gaussian by applying d^2 formula
                            if (autoAdjustRangeSigma)
                            {
                                rangeSigmaForThisPixel = AutoComputeRangeSigma(depthAtPixel, NominalDepthForRangeSigma, rangeSigma);
                            }

                    double val = 0;
                    double normalizer = 0;
                    for (int dy = 0; dy < spatialKernel.GetLength(1); dy++)
                    {
                        for (int dx = 0; dx < spatialKernel.GetLength(0); dx++)
                        {
                                    double depthAtNeighbor = image[x + dx - border, y + dy - border];
                                    if (depthAtNeighbor >= 0)
                                    {
                                        // Compute weight for pixel by combining spatial and range weights
                                        double difference = depthAtPixel - depthAtNeighbor;
                                        double weight = spatialKernel[dx, dy] * Math.Exp(-difference * difference / (2 * rangeSigmaForThisPixel * rangeSigmaForThisPixel));
                            normalizer += weight;

                                        // Accumulate weighted sum
                                        val += depthAtNeighbor * weight;
                                    }
                        }
                    }

                            // Normalize weighted sum and clamp
                    val = Math.Round(val / normalizer);
                    if (val < 0)
                    {
                        val = 0;
                    }

                    if (val > short.MaxValue)
                    {
                        val = short.MaxValue;
                    }

                            // Store normalized weighted sum into output pixel
                    filtered[x, y] = (short)val;
                }
            }
                });

            return filtered;
        }

        /// <summary>
        /// Concatenates a rectangles extracted from a matrix of images into a single image
        /// </summary>
        /// <typeparam name="T">Pixel type</typeparam>
        /// <param name="boundaries">
        /// Specifies the rectangles for each image. 
        /// First index specifies widths, second index the heights
        /// All boundaries in a column must have the same width
        /// All boundaries in a row must have the same height
        /// </param>
        /// <param name="inputImages">Matrix of images</param>
        /// <returns>Concatenated image</returns>
        public static ImageFrame<T> ConcatenateImages<T>(Rectangle[,] boundaries, ImageFrame<T>[,] inputImages) where T : struct
        {
            int width = 0;
            int height = 0;

            // Sanity check column specification
            for (int i = 0; i < boundaries.GetLength(0); ++i)
            {
                int w = boundaries[i, 0].Width;
                width += w;
                for (int j = 1; j < boundaries.GetLength(1); ++j)
                {
                    if (boundaries[i, j].Width != w)
                    {
                        throw new ArgumentException(string.Format("Boundary width inconsistency for rectangle [{0}, {1}], Expected {2} Got {3}", i, j, w, boundaries[i, j].Width));
                    }
                }
            }

            // Sanity check row specification
            for (int j = 0; j < boundaries.GetLength(1); ++j)
            {
                int h = boundaries[0, j].Height;
                height += h;
                for (int i = 1; i < boundaries.GetLength(0); ++i)
                {
                    if (boundaries[i, j].Height != h)
                    {
                        throw new ArgumentException(string.Format("Boundary height inconsistency for rectangle [{0}, {1}], Expected {2} Got {3}", i, j, h, boundaries[j, i].Height));
                    }
                }
            }

            ImageFrame<T> image = new ImageFrame<T>(width, height);

            for (int j = 0, row = 0; j < boundaries.GetLength(1); ++j)
            {
                int rectHeight = boundaries[0, j].Height;
                for (int r = 0; r < rectHeight; ++r, ++row)
                {
                    for (int i = 0, col = 0; i < boundaries.GetLength(0); ++i)
                    {
                        Rectangle rect = boundaries[i, j];
                        for (int c = 0; c < rect.Width; ++c, ++col)
                        {
                            image[col, row] = inputImages[i, j][c + rect.X, r + rect.Y];
                        }
                    }
                }
            }

            return image;
        }

        /// <summary>
        /// Save an image to disk
        /// </summary>
        /// <param name="image">image frame</param>
        /// <param name="filename">name of the file</param>
        public static void SaveImageToFile(ImageFrameGray8 image, string filename)
        {
            BitmapSource bmp = BitmapSource.Create(
                image.Width,
                image.Height,
                96,
                96,
                PixelFormats.Gray8,
                null,
                image.ImageData,
                image.Width);

            using (FileStream stream = new FileStream(filename, FileMode.Create))
            {
                PngBitmapEncoder encoder = new PngBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(bmp));
                encoder.Save(stream);
            }
        }

        /// <summary>
        /// Save an image to disk
        /// </summary>
        /// <param name="image">image frame</param>
        /// <param name="filename">name of the file</param>
        public static void SaveImageToFile(ImageFrameRgb image, string filename)
        {
            byte[] imageBytes = new byte[image.ImageData.Length * image.BytesPerPixel];

            int i = 0;
            foreach (RgbPix p in image.ImageData)
            {
                imageBytes[i++] = p.Blue;
                imageBytes[i++] = p.Green;
                imageBytes[i++] = p.Red;
            }

            BitmapSource bmp = BitmapSource.Create(
                image.Width,
                image.Height,
                96,
                96,
                PixelFormats.Bgr24,
                null,
                imageBytes,
                image.Width * 3);

            using (FileStream stream = new FileStream(filename, FileMode.Create))
            {
                BmpBitmapEncoder encoder = new BmpBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(bmp));
                encoder.Save(stream);
            }
        }

        /// <summary>
        /// Loads an image from a file
        /// </summary>
        /// <param name="filename">The file to load</param>
        /// <returns>A color image</returns>
        public static ImageFrameRgb LoadImageFromFile(string filename)
        {
            using (FileStream stream = new FileStream(filename, FileMode.Open))
            {
                BmpBitmapDecoder decoder = new BmpBitmapDecoder(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
                BitmapSource bitmapSource = decoder.Frames[0];
                byte[] rawData = new byte[bitmapSource.PixelWidth * bitmapSource.PixelHeight * 3];

                bitmapSource.CopyPixels(rawData, bitmapSource.PixelWidth * 3, 0);

                ImageFrameRgb image = new ImageFrameRgb(bitmapSource.PixelWidth, bitmapSource.PixelHeight);

                for (int i = 0, j = 0; i < rawData.Length; i += 3, j++)
                {
                    image.ImageData[j].Red = rawData[i + 2];
                    image.ImageData[j].Green = rawData[i + 1];
                    image.ImageData[j].Blue = rawData[i + 1];
                }

                return image;
            }
        }

        /// <summary>
        /// Add a centre 3x3 cross to an image
        /// </summary>
        /// <param name="image">image frame</param>
        /// <param name="cx">Center X coordinate</param>
        /// <param name="cy">Center Y coordinate</param>
        /// <param name="color">Pixel Color</param>
        public static void Draw3x3Cross(ImageFrameRgb image, double cx, double cy, RgbPix color)
        {
            int ix = (int)Math.Round(cx);
            int iy = (int)Math.Round(cy);

            if (ix > 2 && iy < image.Width - 2 && iy > 2 && iy < image.Height - 2)
            {
                for (int i = -2; i < 2; ++i)
                {
                    image[ix + i, iy] = color;
                    image[ix, iy + i] = color;
                }
            }
        }

        /// <summary>
        /// Adds a line to an image
        /// </summary>
        /// <param name="image">image frame</param>
        /// <param name="startRequest">requested line start point</param>
        /// <param name="endRequest">Line end point</param>
        /// <param name="color">Pixel color</param>
        public static void DrawLine(ImageFrameRgb image, Point2D<int> startRequest, Point2D<int> endRequest, RgbPix color)
        {
            Point2D<int> start = new Point2D<int>(Math.Min(Math.Max(0, startRequest.X), image.Width), Math.Min(Math.Max(0, startRequest.Y), image.Height));
            Point2D<int> end = new Point2D<int>(Math.Min(Math.Max(0, endRequest.X), image.Width), Math.Min(Math.Max(0, endRequest.Y), image.Height));
            double delx = end.X - start.X;
            double dely = end.Y - start.Y;

            if (delx == 0.0 && dely == 0)
            {
                return;
            }

            if (Math.Abs(dely) > Math.Abs(delx))
            {
                int startY = start.Y;
                int endY = end.Y;
                int startX = start.X;

                // Swap start and end
                if (dely < 0)
                {
                    startY = end.Y;
                    startX = end.X;
                    endY = start.Y;
                }

                double inc = delx / dely;
                double x = startX;

                for (int y = startY; y < endY; ++y)
                {
                    image[(int)x, y] = color;
                    x += inc;
                }
            }
            else
            {
                int startX = start.X;
                int endX = end.X;
                int startY = start.Y;

                // Swap start and end
                if (delx < 0)
                {
                    startX = end.X;
                    startY = end.Y;
                    endX = start.X;
                }

                double inc = dely / delx;
                double y = startY;

                for (int x = startX; x < endX; ++x)
                {
                    image[x, (int)y] = color;
                    y += inc;
                }
            }
        }

        /// <summary>
        /// Add a single circle to image, without filling in.
        /// </summary>
        /// <param name="image">image frame</param>
        /// <param name="radius">Circle radius</param>
        /// <param name="cx">center X coordinate</param>
        /// <param name="cy">center Y coordinate</param>
        /// <param name="color">Color of added pixels</param>
        public static void DrawHollowCircle(ImageFrameRgb image, int radius, double cx, double cy, RgbPix color)
        {
            double dt = Math.PI / 2.0 / radius;

            for (int i = 0; i < radius; ++i)
            {
                double theta = dt * i;
                double delX = Math.Cos(theta) * radius;
                double delY = Math.Sin(theta) * radius;
                int ix = (int)Math.Round(cx + delX);
                int iy = (int)Math.Round(cy + delY);
                if (ix >= 0 && ix < image.Width && iy >= 0 && iy < image.Height)
                {
                    image[ix, iy] = color;
                }

                // theta + 90
                ix = (int)Math.Round(cx - delY);
                iy = (int)Math.Round(cy + delX);
                if (ix >= 0 && ix < image.Width && iy >= 0 && iy < image.Height)
                {
                    image[ix, iy] = color;
                }

                // theta + 180
                ix = (int)Math.Round(cx - delX);
                iy = (int)Math.Round(cy - delY);
                if (ix >= 0 && ix < image.Width && iy >= 0 && iy < image.Height)
                {
                    image[ix, iy] = color;
                }

                // theta + 270
                ix = (int)Math.Round(cx + delY);
                iy = (int)Math.Round(cy - delX);
                if (ix >= 0 && ix < image.Width && iy >= 0 && iy < image.Height)
                {
                    image[ix, iy] = color;
                }
            }
        }

        /// <summary>
        /// Find L2 distance between two points in the plane
        /// </summary>
        /// <param name="x1">x coordinate of first point</param>
        /// <param name="y1">y coordinate of first point</param>
        /// <param name="x2">x coordinate of second point</param>
        /// <param name="y2">y coordinate of second point</param>
        /// <returns>Distance between two points</returns>
        public static double L2Distance(double x1, double y1, double x2, double y2)
        {
            double dx = x1 - x2;
            double dy = y1 - y2;
            return Math.Sqrt(dx * dx + dy * dy);
            }

        /// <summary>
        /// Turn RGB image into a binary image
        /// </summary>
        /// <param name="image">Input image</param>
        /// <param name="thresh">Threshold to use</param>
        /// <returns>Binary image</returns>
        public static ImageFrameRgb BinarizeRGB(ImageFrameRgb image, int thresh)
        {
            int len = image.Width * image.Height;
            RgbPix black = new RgbPix(0, 0, 0);
            RgbPix white = new RgbPix(byte.MaxValue, byte.MaxValue, byte.MaxValue);
            ImageFrameRgb binary = new ImageFrameRgb(image.Width, image.Height);

            for (int i = 0; i < len; ++i)
            {
                RgbPix p = image[i];
                double v = (p.Red + p.Green + p.Blue) / 3;
                if (v > thresh)
                {
                    binary[i] = white;
                }
                else
                {
                    binary[i] = black;
                }
            }

            return binary;
        }

        /// <summary>
        /// Pick best low end threshold for binary image
        /// </summary>
        /// <param name="image">Input RGB image</param>
        /// <param name="priorThresh">Prior threshold - assume best threshold is less than this</param>
        /// <returns>Best threshold</returns>
        public static int BestRGBThreshold(ImageFrameRgb image, int priorThresh)
        {
            int len = image.Width * image.Height;
            ushort[] hist = new ushort[byte.MaxValue + 1];

            for (int i = 0; i < len; ++i)
            {
                RgbPix p = image[i];
                byte v = (byte)((p.Red + p.Green + p.Blue) / 3.0);
                hist[v]++;
            }

            int maxCount = hist[0];
            int maxIdx = 0;

            for (int i = 0; i < hist.Length; ++i)
            {
                if (i <= priorThresh && hist[i] > maxCount)
                {
                    maxCount = hist[i];
                    maxIdx = i;
                }
            }

            int minCount = maxCount;
            int minIdx = 0;
            for (int i = maxIdx; i < hist.Length; ++i)
            {
                if (hist[i] < minCount)
                {
                    minIdx = i;
                    minCount = hist[i];
                }
            }

            int blackCount = 0;
            for (int i = maxIdx; i < minIdx; ++i)
            {
                blackCount += hist[i];
            }

            int threshIdx = 0;
            int countThresh = blackCount;
            for (int i = maxIdx, count = 0; count < countThresh; ++i)
            {
                count += hist[i];
                if (count >= countThresh)
                {
                    threshIdx = i;
                    break;
                }
            }

            return threshIdx;
        }

        /// <summary>
        /// Compresses a color image
        /// </summary>
        /// <param name="image">The color image frame to compress</param>
        /// <returns>The compressed image as a byte stream</returns>
        public static byte[] CompressImage(ImageFrameRgb image)
        {
            byte[] imageData = new byte[image.ImageData.Length * 3];
            for (int i = 0; i < image.ImageData.Length; i++)
            {
                imageData[i * 3] = image[i].Red;
                imageData[i * 3 + 1] = image[i].Green;
                imageData[i * 3 + 2] = image[i].Blue;
            }

            BitmapSource src = BitmapSource.Create(image.Width, image.Height, 96, 96, PixelFormats.Bgr24, null, imageData, image.Stride);

            MemoryStream memStream = new MemoryStream();
            JpegBitmapEncoder encoder = new JpegBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(src));
            encoder.Save(memStream);
            return memStream.GetBuffer();
        }

        /// <summary>
        /// Compresses a depth image
        /// </summary>
        /// <param name="image">The depth image to be compressed</param>
        /// <returns>The compressed depth image</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Usage",
            "CA2202:Do not dispose objects multiple times",
            Justification = "'stream' should not be disposed more than once, since parameters to DeflateStream indicate that the stream should not be be disposed when the DeflateStream object is")]
        public static byte[] CompressImage(ImageFrameDepth image)
        {
            byte[] data;

            // Compress Depth image
            unsafe
            {
                fixed (short* bytes = &((short[])image.ImageData)[0])
                {
                    data = new byte[image.ImageData.Length * sizeof(short)];
                    Marshal.Copy(new IntPtr(bytes), data, 0, image.ImageData.Length * sizeof(short));

                    // the compressed data is written to the same buffer to avoid another allocation
                    using (MemoryStream stream = new MemoryStream(data))
                    {
                        using (DeflateStream compressionStream = new DeflateStream(stream, CompressionLevel.Fastest, true))
                        {
                            try
                            {
                                compressionStream.Write(data, 0, data.Length);
                            }
                            catch (NotSupportedException)
                            {
                                // see TFS 1669: if compression results in a larger array, a NotSupportedException is thrown.
                                return null;
                            }
                        }

                        // get the resulting compressed data
                        Array.Resize(ref data, (int)stream.Position);
                    }
                }
            }

            return data;
        }

        /// <summary>
        /// Decompresses a color image
        /// </summary>
        /// <param name="image">The compressed color image</param>
        /// <returns>The decompressed color image</returns>
        public static ImageFrame<RgbPix> DecompressColorImage(byte[] image)
        {
            MemoryStream rgbStream = new MemoryStream(image);
            JpegBitmapDecoder decoder = new JpegBitmapDecoder(rgbStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);

            int width = decoder.Frames[0].PixelWidth;
            int height = decoder.Frames[0].PixelHeight;
            byte[] rgbImage = new byte[width * height * 3];

            decoder.Frames[0].CopyPixels(rgbImage, width * 3, 0);

            ImageFrame<RgbPix> rgbFrame = new ImageFrameRgb(width, height);
            Parallel.For(
                0,
                rgbFrame.ImageData.Length,
                (i) =>
                {
                    int index = i * 3;
                    rgbFrame.ImageData[i].Red = rgbImage[index];
                    rgbFrame.ImageData[i].Green = rgbImage[index + 1];
                    rgbFrame.ImageData[i].Blue = rgbImage[index + 2];
                });

            return rgbFrame;
        }

        /// <summary>
        /// Decompresses a depth image
        /// </summary>
        /// <param name="depth">The compressed depth image</param>
        /// <param name="width">The width of the depth image</param>
        /// <param name="height">the height of the depth image</param>
        /// <returns>The decompressed depth image</returns>
        public static ImageFrame<short> DecompressDepthImage(byte[] depth, int width, int height)
        {
            int count = width * height;
            byte[] bytes = new byte[count * sizeof(short)];

            // decompress the data
            MemoryStream stream = new MemoryStream(depth);
            using (DeflateStream compressionStream = new DeflateStream(stream, CompressionMode.Decompress))
            {
                stream = null;
                compressionStream.Read(bytes, 0, bytes.Length);
            }

            short[] depthImage = new short[count];
            Buffer.BlockCopy(bytes, 0, depthImage, 0, bytes.Length);

            ImageFrame<short> depthFrame = new ImageFrameDepth(width, height);
            for (int i = 0; i < depthImage.Length; ++i)
            {
                depthFrame.ImageData[i] = depthImage[i];
            }

            return depthFrame;
        }
    }
}
